iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0

現在只剩一個問題,要怎樣讓下方的水果卡片,因不同的 filterType 篩選後渲染出來。

腦筋動比較快的人會想到,只要多設一個變數 filterFruits 就好啦!當 TypeSelector 按鈕按下時,更新 filterType ,同時若 filterType 是全部,就讓 filterFruits 複製 fruits 陣列;否則用 filter 來返回正確的顏色。最後將渲染的陣列從 fruits 改成 filterFruits :

function App() {
  const [fruits, setFruits] = useState([...省略]);
  const [filterFruits, setFilterFruits] = useState([...fruits]);
  const [filterType, setFilterType] = useState('全部');

  const handlePress = type => {
    setFilterType(type);
    if (filterType === '全部') {
      setFilterFruits([...fruits]);
    } else {
      const newFruits = fruits.filter(fruit => fruit.color === filterType);
      setFilterFruits([...newFruits]);
    }
  };
  return (
    ...省略
    {filterFruits &&
      filterFruits.map(fruit => (
        <Card
           title={fruit.title}
           imgUrl={fruit.imgUrl}
           key={fruit.title}
        />
  ))}

看似沒問題的程式碼,只要玩玩看就會發現雖然有成功運作,但總是慢一拍。點擊紅色時畫面沒有反應,再點到黃色,篩選出來的卻是紅色。

這時就必須再次回顧前面介紹的 React 更新機制。在前面我們曾提過,為了節省效能, React 會等所有 State 都更新了,才重新以新的 State 來跑一次 Functional Component 並渲染畫面。
https://ithelp.ithome.com.tw/upload/images/20230915/20129635mckPX6YZI2.png

那問題就來了,我們在 handlePress 內,看似有先更新 filterType ,然後根據更新過的 filterType 更新 filterFruits 。但實際上跑到 if (filterType === '全部') 時, filterType 根本還沒更新,必須等到整個 Functional Component 都更新完,才會一次更新並渲染。因為還沒更新,判斷到的也會是更新前的 filterType ,當然也就使整段程式碼慢半拍了。

當然在這個範例中,我們可以輕易把 filterType 的判斷改成 type ,就能避免上述問題:

const handlePress = type => {
  setFilterType(type);
  // 錯誤寫法:
  if (type === '全部') {
    setFilterFruits([...fruits]);
  } else {
    const newFruits = fruits.filter(fruit => fruit.color === type);
    setFilterFruits([...newFruits]);
  }
};

不過若我們的邏輯是,想「監聽」 filterType ,每當 filterType 改變,就去改變 filterFruits ,那有另一個寫法能完成這樣的需求: useEffect 。一個基本的 useEffect 架構如下:

import { useEffect } from 'react';
useEffect(() => {
  if (filterType === '全部') {
    console.log('全部')
  }
}, [filterType]);

第一個參數放箭頭函式,代表如果監聽的對象改變時要執行的事情;第二個參數放一個陣列,裡頭是要監聽的值。例如上面的程式碼會再重新跑一次 Functional Component 時,發現 filterType 改變,執行第一個參數的函式:判斷值是否等於「全部」,是的話執行 console.log()

若第二個參數放空陣列,代表不監聽任何值,則只有元件第一次渲染會執行。相反的,若完全沒放陣列,每次渲染都會跑。

useEffect(() => {
  console.log('只在元件第一次渲染時跑');
}, []);
useEffect(() => {
  console.log('每次渲染時跑');
});

https://ithelp.ithome.com.tw/upload/images/20230915/20129635m9Fnutgq4F.png

要注意的是,每個在第一個參數函式中使用到的變數,都必須放在第二個陣列中,才是正確依賴。即使乍看之下功能好像沒壞掉,也不可以! useEffect 代表一種「副作用」的概念,也就是「因某個變數改變,連帶產生的副作用」。當我們明明用到某變數,卻沒有放在陣列中依賴,可能會使變數值改變時沒同步到,導致難以追蹤的 bug 。

Tips :雖然我一開始也是用生命週期的概念來理解 useEffect ,看不懂副作用是什麼意思,但學到後面發現他不完全等同生命週期,因此還是建議一開始就用正確概念去理解 useEffect 。

如果正確依賴會導致無限迴圈或其他問題,我們該做的應該是想辦法讓第一個參數裡的函式不要使用那些會導致錯誤的變數。因此,不要任意用空陣列來模擬 mounted 的效果!

像下面用到了 filterType 和 fruits ,第二個參數就不能因為任何原因只寫 filterType 或只寫 fruits 。

// 錯誤寫法:
useEffect(() => {
  if (filterType === '全部') {
    console.log(fruits);
  }
}, [filterType]);

// 正確寫法:
useEffect(() => {
  if (filterType === '全部') {
    console.log(fruits);
  }
}, [filterType, fruits]);

回到我們的 Tab 選擇器。將本來的程式碼改成放到 useEffect 並監聽 filterType 和 fruits 。

const [filterFruits, setFilterFruits] = useState([...fruits]);
const [filterType, setFilterType] = useState('全部');

useEffect(() => {
    if (filterType === '全部') {
      setFilterFruits([...fruits]);
    } else {
      const newFruits = fruits.filter(fruit => fruit.color === filterType);
      setFilterFruits([...newFruits]);
    }
}, [filterType, fruits]);

const handlePress = type => {
    setFilterType(type);
};

到這裡為止我們就完成了 Tab 選擇器的所有功能。
https://ithelp.ithome.com.tw/upload/images/20230915/20129635lFxfF2Ydgh.png


參考


上一篇
Day 12. 從實作 Tab 選擇器,認識陣列渲染與動態樣式
下一篇
Day 14. 以 React Navigation 建立路由表、切換頁面
系列文
即使明天老闆突然叫你用 React Native 也可以跟他說好沒問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言